﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using JasLib;
using JasLib.Extensions;

namespace JasLib.Interop.Win32
{
    /// <summary>
    /// The extension methods in this class all throw exceptions on failure, so be warned.
    /// </summary>
    public static class InterfaceExtensions
    {
        #region IContextMenu

        public static bool CommandStringExists(this IContextMenu contextMenu, uint idCmd, bool bCheckUnicode = true)
        {
            uint uiReserved = 0;

            HRESULT hResult = contextMenu.GetCommandString(
                (UIntPtr)idCmd,
                bCheckUnicode ? GetCommandStringCode.GCS_VALIDATEW : GetCommandStringCode.GCS_VALIDATEA,
                ref uiReserved,
                IntPtr.Zero,
                0);

            switch (hResult)
            {
                case HRESULT.S_OK: return true;
                case HRESULT.S_FALSE: return false;
                default: throw hResult.ToCOMException();
            }
        }

        public static string GetCommandString(this IContextMenu contextMenu, uint idCmd, GetCommandStringCode uFlags)
        {
            uint uiReserved = 0;

            switch (uFlags)
            {
                case GetCommandStringCode.GCS_HELPTEXTA:
                case GetCommandStringCode.GCS_VERBA:
                {
                    /// ANSI string.
                    UINT_PTR pBuffer = KERNEL32.HeapAlloc(sizeof(sbyte) * Constants.MAX_PATH);
                    try
                    {
                        HRESULT hResult = contextMenu.GetCommandString((UIntPtr)idCmd, uFlags, ref uiReserved, pBuffer, Constants.MAX_PATH);
                        if (hResult.FAILED())
                            throw hResult.ToCOMException();
                        return Marshal.PtrToStringAnsi(pBuffer);
                    }
                    finally
                    {
                        KERNEL32.HeapFree(pBuffer);
                    }
                }
                case GetCommandStringCode.GCS_HELPTEXTW:
                case GetCommandStringCode.GCS_VERBW:
                {
                    /// Unicode string.
                    UINT_PTR pBuffer = KERNEL32.HeapAlloc(sizeof(char) * Constants.MAX_PATH);
                    try
                    {
                        HRESULT hResult = contextMenu.GetCommandString((UIntPtr)idCmd, uFlags, ref uiReserved, pBuffer, Constants.MAX_PATH);
                        if (hResult.FAILED())
                            throw hResult.ToCOMException();
                        return Marshal.PtrToStringUni(pBuffer);
                    }
                    finally
                    {
                        KERNEL32.HeapFree(pBuffer);
                    }
                }
                case GetCommandStringCode.GCS_VALIDATEW:
                case GetCommandStringCode.GCS_VALIDATEA:
                {
                    throw new InvalidOperationException("GCS_VALIDATEW and GCS_VALIDATEA are invalid codes, instead use the CommandStringExists extension method for this functionality.");
                }
                default:
                {
                    string strMessage = string.Format("{0} is an invalid code for GetCommandString(this IContextMenu)", uFlags);
                    throw new InvalidOperationException(strMessage);
                }
            }
        }

        #endregion

        #region IEnumIDList

        /// <summary>
        /// This is immutable and used frequently. No sense creating unnecessary duplicates.
        /// </summary>
        private static readonly LPITEMIDLIST[] s_emptyItemListArray = new LPITEMIDLIST[0];

        public static LPITEMIDLIST[] Next(this IEnumIDList enumIdList, int iTryCount)
        {
            UINT_PTR[] ppItemIdList = new UINT_PTR[iTryCount];
            uint uiFetchedCount = 0;
            HRESULT hResult = enumIdList.Next((uint)ppItemIdList.Length, ppItemIdList, out uiFetchedCount);
            if (hResult != HRESULT.S_OK && hResult != HRESULT.S_FALSE)
                throw hResult.ToCOMException();

            if (uiFetchedCount == 0)
                return s_emptyItemListArray;
            else if (uiFetchedCount == (uint)iTryCount)
                return ppItemIdList.ConvertAll(i => new LPITEMIDLIST(i));
            else
            {
                LPITEMIDLIST[] ppTruncated = new LPITEMIDLIST[uiFetchedCount];
                for (uint uiIndex = 0; uiIndex < uiFetchedCount; uiIndex++)
                    ppTruncated[uiIndex] = new LPITEMIDLIST(ppItemIdList[uiIndex]);
                return ppTruncated;
                //return ppItemIdList.Take((int)uiFetchedCount).Select(i => new LPITEMIDLIST(i)).ToArray();
            }
        }

        #endregion

        #region IFileOperation

        public static void SetOperationFlags(this IFileOperation fileOperation, FILEOP_FLAGS dwOperationFlags)
        {
            HRESULT hResult = fileOperation.SetOperationFlags((uint)dwOperationFlags);
            hResult.TryThrowCOMException();
            return;
        }

        #endregion

        #region IShellFolder

        /// <summary>
        /// General-purpose extension function that mitigates the complexity of IShellFolder.EnumObjects().
        /// Each returned IntPtr is a pointer to an ITEMIDLIST and must be freed with CoTaskMemFree().
        /// Pointers cannot be used as generic parameters, although this still would work fine in a foreach loop.
        /// </summary>
        public static IEnumerable<LPITEMIDLIST> EnumObjects(this IShellFolder shellFolder, HWND hParentWindow, SHCONTF grfFlags = SHCONTF.SHCONTF_FOLDERS | SHCONTF.SHCONTF_NONFOLDERS)
        {
            IEnumIDList enumIdList = null;

            try
            {
                HRESULT hResult = shellFolder.EnumObjects(hParentWindow, grfFlags, out enumIdList);
                if (hResult.FAILED())
                    throw new COMException(string.Empty, hResult);

                while (true)
                {
                    LPITEMIDLIST[] ppItemIdList = enumIdList.Next(1);
                    if (ppItemIdList.Length == 0)
                        break;

                    for (int iIndex = 0; iIndex < ppItemIdList.Length; iIndex++)
                        yield return ppItemIdList[iIndex];
                }
            }
            finally
            {
                if (enumIdList != null)
                {
                    Marshal.ReleaseComObject(enumIdList);
                    enumIdList = null;
                }
            }

            yield break;
        }

        /// <summary>
        /// General-purpose extension function that mitigates the complexity of IShellFolder.GetAttributesOf().
        /// </summary>
        /// <returns>the tested flags which are common to every provided ITEMIDLIST*</returns>
        public static SFGAOF GetAttributesOf(this IShellFolder shellFolder, SFGAOF flagsToTest, params LPITEMIDLIST[] apidl)
        {
            HRESULT hResult = shellFolder.GetAttributesOf((uint)apidl.Length, apidl.ToIntPtrArray(), ref flagsToTest);
            if (hResult.FAILED())
                throw hResult.ToCOMException();
            return flagsToTest;
        }

        /// <summary>
        /// General-purpose extension function that mitigates the complexity of IShellFolder.GetDisplayNameOf().
        /// </summary>
        /// <param name="itemIdList">If null, this returns the name of the shell folder itself.</param>
        public static string GetDisplayNameOf(this IShellFolder shellFolder, LPITEMIDLIST itemIdList, SHGDNF uFlags = SHGDNF.SHGDN_NORMAL)
        {
            STRRET returnedString;
            HRESULT hResult = shellFolder.GetDisplayNameOf(itemIdList, uFlags, out returnedString);
            if (hResult.FAILED())
                throw hResult.ToCOMException();

            UINT_PTR pszName;
            hResult = SHLWAPI.StrRetToStr(ref returnedString, itemIdList, out pszName);
            hResult.TryThrowCOMException();

            try
            {
                return Marshal.PtrToStringUni(pszName);
            }
            finally
            {
                OLE32.CoTaskMemFree(pszName);
            }
        }

        public static void BindToObject(this IShellFolder shellFolder, LPITEMIDLIST itemIdList, out IShellFolder shellSubFolder, IBindCtx pbc = null)
        {
            object objShellSubFolder;
            HRESULT hResult = shellFolder.BindToObject(itemIdList, pbc, Constants.Guids.IID_IShellFolder, out objShellSubFolder);
            hResult.TryThrowCOMException();
            shellSubFolder = (objShellSubFolder as IShellFolder);
            return;
        }

        public static int CompareIDs(this IShellFolder shellFolder, LPITEMIDLIST pidl1, LPITEMIDLIST pidl2, SHCIDS shcids = SHCIDS.SHCIDS_ALLFIELDS)
        {
            return (int)(short)shellFolder.CompareIDs((IntPtr)shcids, pidl1, pidl2).HRESULT_CODE();
        }

        #endregion

        #region IShellItem

        public static int Compare(this IShellItem shellItem, IShellItem otherShellItem, SICHINTF hint = SICHINTF.SICHINT_ALLFIELDS)
        {
            int iOrder;
            HRESULT hResult = shellItem.Compare(otherShellItem, hint, out iOrder);
            if (hResult != HRESULT.S_OK && hResult != HRESULT.S_FALSE)
                throw hResult.ToCOMException();
            return iOrder;
        }

        public static SFGAOF GetAttributes(this IShellItem shellItem, SFGAOF flagsToTest)
        {
            SFGAOF actualFlags;
            HRESULT hResult = shellItem.GetAttributes(flagsToTest, out actualFlags);
            if (hResult != HRESULT.S_OK && hResult != HRESULT.S_FALSE)
                throw hResult.ToCOMException();
            return actualFlags;
        }

        public static string GetDisplayName(this IShellItem shellItem, SIGDN sigdnName = SIGDN.SIGDN_NORMALDISPLAY)
        {
            UINT_PTR pszName = UINT_PTR.Zero;
            try
            {
                HRESULT hResult = shellItem.GetDisplayName(sigdnName, out pszName);
                hResult.TryThrowCOMException();
                return Marshal.PtrToStringUni(pszName);
            }
            finally
            {
                if (pszName != UINT_PTR.Zero)
                    OLE32.CoTaskMemFree(pszName);
            }
        }

        #endregion
    }
}